/*
 * @(#)DataTypeValidator.java
 * Date 2013-09-29
 * Version 1.0
 * Author Jim Horner
 * Copyright (c)2012
 */


package us.hornerscorners.lamppost.registry.model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.enterprise.context.ApplicationScoped;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

import org.joda.time.DateTime;

import us.hornerscorners.lamppost.exception.ServiceActionError;
import us.hornerscorners.lamppost.exception.SeverityType;
import us.hornerscorners.lamppost.exception.ValidationException;

import us.hornerscorners.lamppost.registry.constant.ErrorCode;
import us.hornerscorners.lamppost.registry.constant.Metadata;
import us.hornerscorners.lamppost.registry.constant.QuerySlotName;
import us.hornerscorners.lamppost.registry.constant.SlotName;
import us.hornerscorners.lamppost.registry.constant.XDSDataType;

import us.hornerscorners.lamppost.registry.utils.XDSUtils;

import us.hornerscorners.lamppost.utils.OIDUtils;
import us.hornerscorners.lamppost.utils.UUIDUtils;


/**
 * Class description
 *
 *
 * @version        v1.0, 2013-09-29
 * @author         Jim Horner
 */
@ApplicationScoped
public class DataTypeValidator {

    /** Field description */
    private final Pattern sha1Pattern = Pattern.compile("^[0-9A-Fa-f]+$");

    /**
     * Constructs ...
     *
     */
    public DataTypeValidator() {
        super();
    }

    /**
     * Method description
     *
     *
     * @param metadata
     * @param dataType
     *
     */
    public Collection<ServiceActionError> validate(Metadata metadata,
            XDSDataType dataType, Collection<String> values) {

        return validate(metadata.name(), dataType, values);
    }

    /**
     * Method description
     *
     *
     * @param metadata
     * @param dataType
     * @param value
     *
     */
    public Collection<ServiceActionError> validate(Metadata metadata,
            XDSDataType dataType, String value) {

        return validate(metadata.name(), dataType, value);
    }

    /**
     * Method description
     *
     *
     * @param slotName
     * @param dataType
     *
     */
    public Collection<ServiceActionError> validate(QuerySlotName slotName,
            XDSDataType dataType, Collection<String> values) {

        return validate(slotName.getCode(), dataType, values);
    }

    /**
     * Method description
     *
     *
     * @param slotName
     * @param dataType
     * @param value
     *
     */
    public Collection<ServiceActionError> validate(QuerySlotName slotName,
            XDSDataType dataType, String value) {

        return validate(slotName.getCode(), dataType, value);
    }

    /**
     * Method description
     *
     *
     * @param dataType
     *
     */
    public Collection<ServiceActionError> validate(SlotName slotName,
            XDSDataType dataType, Collection<String> values) {

        return validate(slotName.getCode(), dataType, values);
    }

    /**
     * Method description
     *
     *
     * @param dataType
     * @param value
     *
     */
    public Collection<ServiceActionError> validate(SlotName slotName,
            XDSDataType dataType, String value) {

        return validate(slotName.getCode(), dataType, value);
    }

    /**
     * Method description
     *
     *
     * @param slotName
     * @param dataType
     * @param values
     *
     */
    private Collection<ServiceActionError> validate(String slotName,
            XDSDataType dataType, Collection<String> values) {

        List<ServiceActionError> errors = new ArrayList<>();

        for (String value : values) {

            errors.addAll(validate(slotName, dataType, value));
        }

        return errors;
    }

    /**
     * Method description
     *
     *
     * @param slotName
     * @param dataType
     * @param value
     *
     */
    private Collection<ServiceActionError> validate(String slotName,
            XDSDataType dataType, String value) {

        List<ServiceActionError> errors = new ArrayList<>();

        if (StringUtils.isNotBlank(value)) {

            switch (dataType) {

                case ce :
                    errors.addAll(validateCodedElement(slotName, value));

                    break;

                case cx :
                    errors.addAll(validateIdentifier(slotName, value));

                    break;

                case dtm :
                    errors.addAll(validateDateTime(slotName, value));

                    break;

                case field :

                    // validateMessageField(slotName, value);

                    break;

                case oid :
                    errors.addAll(validateOid(slotName, value));

                    break;

                case sha1 :

                    errors.addAll(validateSha1(slotName, value));

                    break;

                case string :

                    // validateString(slotName, value);

                    break;

                case uri :

                    // validateUri(slotName, value);

                    break;

                case uuid :
                    errors.addAll(validateUuid(slotName, value));

                    break;

                case xcn :
                    errors.addAll(validateExtendedName(slotName, value));

                    break;

                case xon :
                    errors.addAll(validateOrganization(slotName, value));

                    break;

                case xtn :
                    errors.addAll(validateTelecom(slotName, value));

                    break;
            }
        }

        return errors;
    }

    /**
     * Method description
     *
     *
     * @param slotName
     * @param value
     *
     */
    private Collection<ServiceActionError> validateAssigningAuthority(String slotName,
            String value) {

        List<ServiceActionError> errors = new ArrayList<>();

        String[] rootarr = StringUtils.splitPreserveAllTokens(value, "&");

        if (ArrayUtils.isEmpty(rootarr) || (rootarr.length < 3)) {

            String msg =
                String.format(
                    "Slot [%s] assigning authority is empty or does not contain enough items. String [%s] has %d pieces.",
                    slotName, value, rootarr.length);

            ServiceActionError error =
                new ServiceActionError(ErrorCode.XDSRegistryMetadataError.getCode(), msg,
                                       SeverityType.Error);

            errors.add(error);

        } else {

            // TODO handle mapping other types (DNS, etc) to OID
            // see ITI-2x Appendix E
            // ITI-3 4.1.7, constrains CX to OID
            if (OIDUtils.isNotValid(rootarr[1])) {

                String msg =
                    String.format("Slot [%s] value [%s] does not contain enough pieces.",
                                  slotName, value);

                ServiceActionError error =
                    new ServiceActionError(ErrorCode.XDSRegistryMetadataError.getCode(),
                                           msg, SeverityType.Error);

                errors.add(error);
            }

            if ("ISO".equals(rootarr[2]) == false) {

                String msg = String.format("Slot [%s] value [%s] is not of type ISO.",
                                           slotName, value);

                ServiceActionError error =
                    new ServiceActionError(ErrorCode.XDSRegistryMetadataError.getCode(),
                                           msg, SeverityType.Error);

                errors.add(error);
            }
        }

        return errors;
    }

    /**
     * Method description
     *
     *
     * @param value
     */
    private Collection<ServiceActionError> validateCodedElement(String slotName,
            String value) {

        List<ServiceActionError> errors = new ArrayList<>();

        String code = null;
        String scheme = null;

        if (StringUtils.contains(value, "^")) {

            String[] arr = StringUtils.splitPreserveAllTokens(value, "^");

            if ((arr == null) || (arr.length != 3)) {

                String msg =
                    String.format(
                        "Slot [%s] value [%s] does not contain enough pieces for coded element; should be 3.",
                        slotName, value);

                ServiceActionError error =
                    new ServiceActionError(ErrorCode.XDSRegistryMetadataError.getCode(),
                                           msg, SeverityType.Error);

                errors.add(error);

            } else {

                if (StringUtils.isNotBlank(arr[1])) {

                    String msg =
                        String.format(
                            "Slot [%s] value [%s] is not valid for coded element; piece 2 must be empty.",
                            slotName, value);

                    ServiceActionError error =
                        new ServiceActionError(
                            ErrorCode.XDSRegistryMetadataError.getCode(), msg,
                            SeverityType.Error);

                    errors.add(error);
                }

                code = arr[0];
                scheme = arr[2];
            }

        } else {

            code = value;
        }

        if (StringUtils.isBlank(code) && StringUtils.isBlank(scheme)) {

            String msg =
                String.format(
                    "Slot [%s] value [%s] is invalid for coded element; code or scheme is required.",
                    slotName, value);

            ServiceActionError error =
                new ServiceActionError(ErrorCode.XDSRegistryMetadataError.getCode(), msg,
                                       SeverityType.Error);

            errors.add(error);
        }

        return errors;
    }

    /**
     * Method description
     *
     *
     * @param slotName
     * @param value
     */
    private Collection<ServiceActionError> validateDateTime(String slotName,
            String value) {

        List<ServiceActionError> errors = new ArrayList<>();

        if (StringUtils.isNotBlank(value)) {

            DateTime dtm = XDSUtils.parseDTM(value);

            if (dtm == null) {

                String msg =
                    String.format("Slot [%s] value [%s] is not a valid date/time.",
                                  slotName, value);

                ServiceActionError error =
                    new ServiceActionError(ErrorCode.XDSRegistryMetadataError.getCode(),
                                           msg, SeverityType.Error);

                errors.add(error);
            }
        }

        return errors;
    }

    /**
     * Method description
     *
     *
     * @param slotName
     * @param value
     */
    private Collection<ServiceActionError> validateExtendedName(String slotName,
            String value) {

        List<ServiceActionError> errors = new ArrayList<>();

        String[] arr = StringUtils.splitPreserveAllTokens(value, "^");

        if ((arr == null) || (arr.length < 2)) {

            String msg =
                String.format("Slot [%s] value [%s] does not contain enough pieces.",
                              slotName, value);

            ServiceActionError error =
                new ServiceActionError(ErrorCode.XDSRegistryMetadataError.getCode(), msg,
                                       SeverityType.Error);

            errors.add(error);

        } else {

            // name or identifier is required
            // TOOD which name??
            String id = arr[0];
            String name = arr[1];

            if (StringUtils.isBlank(id) && StringUtils.isBlank(name)) {

                String msg =
                    String.format(
                        "Slot [%s] value [%s] must contain either a name or identifier.",
                        slotName, value);

                ServiceActionError error =
                    new ServiceActionError(ErrorCode.XDSRegistryMetadataError.getCode(),
                                           msg, SeverityType.Error);

                errors.add(error);

            } else if (StringUtils.isNotBlank(id)) {

                if ((arr.length < 8) || StringUtils.isBlank(arr[8])) {

                    String msg =
                        String.format(
                            "Slot [%s] value [%s] must contain assigning authority with the identifier.",
                            slotName, value);

                    ServiceActionError error =
                        new ServiceActionError(
                            ErrorCode.XDSRegistryMetadataError.getCode(), msg,
                            SeverityType.Error);

                    errors.add(error);

                } else {

                    errors.addAll(validateAssigningAuthority(slotName, arr[8]));
                }
            }
        }

        return errors;
    }

    /**
     * Method description
     *
     *
     * @param value
     */
    private Collection<ServiceActionError> validateIdentifier(String slotName,
            String value) {

        List<ServiceActionError> errors = new ArrayList<>();

        String[] pieces = StringUtils.splitPreserveAllTokens(value, "^");

        if (ArrayUtils.isEmpty(pieces) || (pieces.length < 4)) {

            String msg =
                String.format(
                    "Slot [%s] is empty or does not contain 3 pieces. String [%s] has %d pieces.",
                    slotName, value, pieces.length);

            ServiceActionError error =
                new ServiceActionError(ErrorCode.XDSRegistryMetadataError.getCode(), msg,
                                       SeverityType.Error);

            errors.add(error);

        } else {

            errors.addAll(validateAssigningAuthority(slotName, pieces[3]));
        }

        return errors;
    }

    /**
     * Method description
     *
     *
     * @param slotName
     * @param value
     *
     */
    private Collection<ServiceActionError> validateOid(String slotName, String value) {

        List<ServiceActionError> errors = new ArrayList<>();

        if (OIDUtils.isNotValid(value)) {

            String msg =
                String.format(
                    "Slot [%s] contains invalid value - [%s] is not a valid OID.",
                    slotName, value);

            ServiceActionError error =
                new ServiceActionError(ErrorCode.XDSRegistryMetadataError.getCode(), msg,
                                       SeverityType.Error);

            errors.add(error);
        }

        return errors;
    }

    /**
     * Method description
     *
     *
     * @param slotName
     * @param value
     */
    private Collection<ServiceActionError> validateOrganization(String slotName,
            String value) {

        List<ServiceActionError> errors = new ArrayList<>();
        String[] arr = StringUtils.splitPreserveAllTokens(value, "^");

        if ((arr == null) || (arr.length < 1) || StringUtils.isBlank(arr[0])) {

            String msg =
                String.format("Slot [%s] value [%s] does not contain enough pieces.",
                              slotName, value);

            ServiceActionError error =
                new ServiceActionError(ErrorCode.XDSRegistryMetadataError.getCode(), msg,
                                       SeverityType.Error);

            errors.add(error);

        } else {

            String xon6 = null;
            String xon10 = null;

            for (int i = 1; i < arr.length; ++i) {

                if (i == 5) {

                    xon6 = arr[i];

                } else if (i == 9) {

                    xon10 = arr[i];

                } else if (StringUtils.isNotBlank(arr[i])) {

                    // all others should be blank
                    String msg =
                        String.format(
                            "Slot [%s] value [%s] is not valid; pieces 2, 3, 4, 5, 7, 8, 9 must be blank.",
                            slotName, value);

                    ServiceActionError error =
                        new ServiceActionError(
                            ErrorCode.XDSRegistryMetadataError.getCode(), msg,
                            SeverityType.Error);

                    errors.add(error);
                }
            }

            if (StringUtils.isNotBlank(xon10) && OIDUtils.isNotValid(xon10)) {

                if (StringUtils.isBlank(xon6)) {

                    String msg =
                        String.format(
                            "Slot [%s] value [%s] is not valid; xon6 is required when xon10 is an identifier.",
                            slotName, value);

                    ServiceActionError error =
                        new ServiceActionError(
                            ErrorCode.XDSRegistryMetadataError.getCode(), msg,
                            SeverityType.Error);

                    errors.add(error);

                } else {

                    validateAssigningAuthority(slotName, xon6);
                }
            }
        }

        return errors;
    }

    /**
     * Method description
     *
     *
     * @param slotName
     * @param value
     */
    private Collection<ServiceActionError> validateSha1(String slotName, String value) {

        List<ServiceActionError> errors = new ArrayList<>();
        Matcher matcher = sha1Pattern.matcher(value);

        if (matcher.matches() == false) {

            String msg =
                String.format(
                    "Slot [%s] contains invalid value - [%s] is not a valid SHA1 hash.",
                    slotName, value);

            ServiceActionError error =
                new ServiceActionError(ErrorCode.XDSRegistryMetadataError.getCode(), msg,
                                       SeverityType.Error);

            errors.add(error);
        }

        return errors;
    }

    /**
     * Method description
     *
     *
     * @param slotName
     * @param value
     */
    private Collection<ServiceActionError> validateTelecom(String slotName,
            String value) {

        List<ServiceActionError> errors = new ArrayList<>();

        String[] arr = StringUtils.splitPreserveAllTokens(value, "^");

        if ((arr == null) || (arr.length < 4)) {

            String msg =
                String.format("Slot [%s] value [%s] does not contain enough pieces.",
                              slotName, value);

            ServiceActionError error =
                new ServiceActionError(ErrorCode.XDSRegistryMetadataError.getCode(), msg,
                                       SeverityType.Error);

            errors.add(error);

        } else {

            if (StringUtils.isNotBlank(arr[0]) || StringUtils.isNotBlank(arr[1])) {

                String msg =
                    String.format(
                        "Slot [%s] value [%s] is invalid; xon1 and xon2 must be blank.",
                        slotName, value);

                ServiceActionError error =
                    new ServiceActionError(ErrorCode.XDSRegistryMetadataError.getCode(),
                                           msg, SeverityType.Error);

                errors.add(error);
            }

            if (StringUtils.isBlank(arr[2]) || StringUtils.isBlank(arr[3])) {

                String msg =
                    String.format(
                        "Slot [%s] value [%s] is invalid; xon3 and xon4 are required.",
                        slotName, value);

                ServiceActionError error =
                    new ServiceActionError(ErrorCode.XDSRegistryMetadataError.getCode(),
                                           msg, SeverityType.Error);

                errors.add(error);
            }
        }

        return errors;
    }

    /**
     * Method description
     *
     *
     * @param slotName
     * @param value
     */
    private Collection<ServiceActionError> validateUuid(String slotName, String value) {

        List<ServiceActionError> errors = new ArrayList<>();

        if (UUIDUtils.isNotValidWithURN(value)) {

            String msg =
                String.format(
                    "Slot [%s] contains invalid value; [%s] is not a valid UUID.",
                    slotName, value);

            ServiceActionError error =
                new ServiceActionError(ErrorCode.XDSRegistryMetadataError.getCode(), msg,
                                       SeverityType.Error);

            errors.add(error);
        }

        return errors;
    }
}
